package com.google.code.sms.service.metadata;

import com.google.code.sms.Logger;
import com.google.code.sms.io.InputStreamReaderThread;
import com.google.code.sms.service.TranscodingService;
import com.google.code.sms.util.StringUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import com.google.code.sms.util.FileUtil;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;

/**
 * Parses meta-data from video files using FFmpeg.
 * <p/>
 *
 * @author Scott Ware
 */
public class VideoParser {

    private static final Logger LOG = Logger.getLogger("VideoParser");
    
    private static final Pattern DURATION = Pattern.compile("Duration: (\\d+):(\\d+):(\\d+).(\\d+)");
    private static final Pattern BITRATE = Pattern.compile("bitrate: (\\d+) kb/s");
    private static final Pattern DIMENSION = Pattern.compile("Video.*?, (\\d+)x(\\d+)");
    private static final Pattern VIDEO_CODEC = Pattern.compile("Video:\\s*(\\S+)");
    private static final Pattern AUDIO_PARAMETERS = Pattern.compile("Audio:\\s*(\\S+).*[,]\\s*(\\d+) Hz,\\s*([\\w\\.]+)");
    private static final Pattern TITLE_YEAR = Pattern.compile ("(.+)\\s+[(](\\d{4})[)$]");
    
    // An XML file in the parent directory with additional information about the file.
    private String DESCRIPTION_FILE_NAME = "nfo";
    
    private TranscodingService transcodingService;

    /**
     * Parses meta data for the given video file.
     *
     *
     * @param file The video file to parse.
     * @return Meta data for the file.
     */
    public MetaData parseMetaData(File file)
    {

        MetaData metaData = new MetaData();

        /* 
         * FFMPEG File Information
         * 
         * Duration, Bitrate, Dimensions, Video Codec, Audio Parameters
         */
        try {

            File transcoder = transcodingService.getTranscoder();

            String[] command = new String[]{transcoder.getAbsolutePath(), "-i", file.getAbsolutePath()};
            Process process = Runtime.getRuntime().exec(command);
            InputStream stdout = process.getInputStream();
            InputStream stderr = process.getErrorStream();

            // Consume stdout, we're not interested in that.
            new InputStreamReaderThread(stdout, "ffmpeg", true).start();

            // Read everything from stderr.
            String[] lines = StringUtil.readLines(stderr);

            Matcher matcher;
                    
            for (String line : lines) {

                if(metaData.getDuration() == null)
                {
                    // Search for video duration
                    matcher = DURATION.matcher(line);

                    if (matcher.find()) 
                    {
                        int hours = Integer.parseInt(matcher.group(1));
                        int minutes = Integer.parseInt(matcher.group(2));
                        int seconds = Integer.parseInt(matcher.group(3));
                        metaData.setDuration(hours * 3600 + minutes * 60 + seconds);
                    }
                }

                // Search for video bitrate
                if(metaData.getBitRate() == null)
                {
                    matcher = BITRATE.matcher(line);

                    if (matcher.find()) 
                    {
                        metaData.setBitRate(Integer.parseInt(matcher.group(1)));
                    }
                }

                // Search for video dimensions
                if(metaData.getWidth() == null || metaData.getHeight() == null)
                {
                    matcher = DIMENSION.matcher(line);

                    if (matcher.find()) 
                    {
                        int width = Integer.parseInt(matcher.group(1));
                        int height = Integer.parseInt(matcher.group(2));

                        if(width > 0 && height > 0)
                        {
                            metaData.setWidth(width);
                            metaData.setHeight(height);
                        }
                    }
                }
                
                // Search for video codec
                if(metaData.getVideoCodec() == null)
                {
                    matcher = VIDEO_CODEC.matcher(line);
                    
                    if (matcher.find())
                    {
                        metaData.setVideoCodec(String.valueOf(matcher.group(1)));
                    }
                }
                
                // Search for audio parameters
                if(metaData.getAudioCodec() == null || metaData.getAudioSampleRate() == null || metaData.getAudioConfiguration() == null)
                {
                    matcher = AUDIO_PARAMETERS.matcher(line);

                    if (matcher.find()) 
                    {
                        metaData.setAudioCodec(String.valueOf(matcher.group(1)));
                        metaData.setAudioSampleRate(Integer.parseInt(matcher.group(2)));
                        metaData.setAudioConfiguration(String.valueOf(matcher.group(3)));
                    }
                }
            }

        } 
        catch (IOException | NumberFormatException x)
        {
            LOG.warn("Error when parsing metadata for: " + file, x);
        }
        
        /*
        * XML Descriptor File (if it exists)
        * 
        * Rating, Plot, Tagline, Certificate, Title, Year
        */
        try {
 
            // Get XML file
            File descriptionFile = getDescriptionFile(file.getParent());
            
            if(descriptionFile != null)
            {
                DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = dbFactory.newDocumentBuilder();
                Document document = builder.parse(descriptionFile);

                // Optimise XML before proceeding to minimise errors
                document.getDocumentElement().normalize();

                 if(metaData.getTitle() == null && document.getElementsByTagName("title").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("title").item(0).getTextContent().equals(""))
                     {
                        metaData.setTitle(document.getElementsByTagName("title").item(0).getTextContent());
                     }
                 }
                 
                 if(metaData.getRating() == null && document.getElementsByTagName("rating").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("rating").item(0).getTextContent().equals(""))
                     {
                        Float rating = Float.valueOf(document.getElementsByTagName("rating").item(0).getTextContent());
                        metaData.setRating(rating);
                     }
                 }
                 
                 if(metaData.getYear() == null && document.getElementsByTagName("year").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("year").item(0).getTextContent().equals(""))
                     {
                        Integer year = Integer.valueOf(document.getElementsByTagName("year").item(0).getTextContent());
                        metaData.setYear(year);
                     }
                 }
                 
                 if(metaData.getDescription() == null && document.getElementsByTagName("plot").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("plot").item(0).getTextContent().equals(""))
                     {
                        metaData.setDescription(document.getElementsByTagName("plot").item(0).getTextContent());
                     }
                 }
                 
                 if(metaData.getTagline() == null && document.getElementsByTagName("tagline").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("tagline").item(0).getTextContent().equals(""))
                     {
                        metaData.setTagline(document.getElementsByTagName("tagline").item(0).getTextContent());
                     }
                 }
                 
                 if(metaData.getCertificate() == null && document.getElementsByTagName("mpaa").getLength() > 0)
                 {
                     if(!document.getElementsByTagName("mpaa").item(0).getTextContent().equals(""))
                     {
                        metaData.setCertificate(document.getElementsByTagName("mpaa").item(0).getTextContent());
                     }
                 }
            }
        } 
        catch (Exception e)
        {
            LOG.warn("Error when parsing XML description file for: " + file, e);
        }
        
        /*
         * File Name (Last Resort)
         * 
         * Title, Year
         */
        if(metaData.getTitle() == null && metaData.getYear() == null)
        {
            Matcher matcher = TITLE_YEAR.matcher(StringUtils.trim(FilenameUtils.getBaseName(file.getPath())));

            if(matcher.find())
            {
                metaData.setTitle(String.valueOf(matcher.group(1)));
                metaData.setYear(Integer.parseInt(matcher.group(2)));
            }
            else
            {
                metaData.setTitle(StringUtils.trim(FilenameUtils.getBaseName(file.getPath())));
            }
        }

        return metaData;
    }

    // Returns an XML description file in the parent directory if present.
    private File getDescriptionFile(String path)
    {
        // Recursively check for description XML file in given directory path
        for (File candidate : new File(path).listFiles()) 
        {
            if (candidate.isFile() && !FileUtil.isHidden(candidate) && candidate.getName().toUpperCase().endsWith(DESCRIPTION_FILE_NAME.toUpperCase())) 
            {
                return candidate;
            }
        }
        
        return null;
    }
    
    public void setTranscodingService(TranscodingService transcodingService) {
        this.transcodingService = transcodingService;
    }
}